package org.shagin.jfin.budget;

import java.io.IOException;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.lang3.StringUtils;
import org.shagin.jfin.core.Debug;
import org.shagin.jfin.core.CoreUtils;
import org.shagin.jfin.data.Money;
import org.shagin.jfin.data.model.Account;
import org.shagin.jfin.data.model.Budget;
import org.shagin.jfin.data.model.CostCenter;
import org.shagin.jfin.data.model.IBudget;
import org.shagin.jfin.data.model.OperationCategory;
import org.shagin.jfin.data.model.OperationCategoryLeaf;
import org.shagin.jfin.data.model.OperationRule;
import org.shagin.jfin.data.model.domain.AccountDomain;
import org.shagin.jfin.data.model.domain.CostCenterDomain;
import org.shagin.jfin.data.model.domain.OperationRuleDomain;
import org.shagin.jfin.date.DateUtils;
import org.shagin.jfin.date.cond.DateRange;
import org.shagin.jfin.date.cond.xml.OperationRuleXmlParser;
import org.shagin.xml.XMLUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * The Class BudgetXMLReader.
 */
public class BudgetXMLReader extends BudgetReader implements IBudgetXML {

    private static final Logger log = LoggerFactory
            .getLogger(BudgetXMLReader.class);

    private String fileName;

    private Document doc;
    private CostCenter defaultCostCenter = null;
    private Account defaultAccount = null;
    private Budget budget;

    /**
     * Instantiates a new budget xml reader.
     * 
     * @param fileName the file name
     */
    public BudgetXMLReader(String fileName) {
        CoreUtils.checkNotNull(fileName, "fileName");
        this.fileName = fileName;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IBudget read() {
        budget = new Budget(DEFAULT_ID);
        Debug.traceEnter(log);
        loadXml();
        loadRootAttributes();
        loadCostCenters();
        loadAccounts();
        loadIncomes();
        loadExpenses();
        loadTrasfers();
        return budget;
    }

    /**
     * Load xml.
     */
    private void loadXml() {
        try {
            doc = XMLUtils.loadXmlDocument(fileName);
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Load date range.
     */
    private void loadRootAttributes() {
        Node node = XMLUtils.fundNodeByPath(XMLUtils.getDocumentRoot(doc),
                "/budget");

        DateRange range = Budget.getDefaultDateRange();
        range.setStart(DateUtils.parse(getNodeAttribute(node, "from",
                DateUtils.format(range.getStart()))));
        range.setEnd(DateUtils.parse(getNodeAttribute(node, "to",
                DateUtils.format(range.getEnd()))));
        budget.getDateRange().assign(range);
        budget.setId(getNodeAttribute(node, "id", DEFAULT_ID));
    }

    /**
     * Gets the node attribute.
     * 
     * @param node the node
     * @param attr the attr
     * @param defValue the def value
     * @return the node attribute
     */
    private static String getNodeAttribute(Node node, String attr,
            String defValue) {
        String result = defValue;
        if (node != null) {
            String v = XMLUtils.getNodeAttribute(node, attr, StringUtils.EMPTY);
            if (StringUtils.isEmpty(v))
                result = getNodeAttribute(node.getParentNode(), attr, defValue);
            else
                result = v;
        }
        return result;
    }

    /**
     * Gets the cost center.
     * 
     * @param node the node
     * @return the cost center
     */
    private CostCenter getCostCenter(Node node) {
        String ccId = getNodeAttribute(node, "CostCenter",
                defaultCostCenter.getId());
        CostCenter cc = budget.getCostCenterDomain().findById(ccId);
        if (cc == null)
            throw new RuntimeException("Can not found CostCenter : " + ccId);
        return cc;
    }

    /**
     * Checks if is default node.
     * 
     * @param node the node
     * @return true, if is default node
     */
    private static boolean isDefaultNode(Node node) {
        String value = XMLUtils.getNodeAttribute(node, "default", "");
        return "true".equalsIgnoreCase(value);
    }

    /**
     * Load cost centers.
     */
    private void loadCostCenters() {
        CostCenterDomain ccd = budget.getCostCenterDomain();
        Node node = XMLUtils.fundNodeByPath(XMLUtils.getDocumentRoot(doc),
                "/budget/CostCenters");
        List<Node> childs = XMLUtils.getChildNodes(node);
        for (Node n : childs) {
            if (XMLUtils.isNodeNameEquals(n, "CostCenter")) {
                CostCenter cc = ccd.add(XMLUtils.getNodeId(n));
                if (defaultCostCenter == null && isDefaultNode(n))
                    defaultCostCenter = cc;
            }
        }

        if (defaultCostCenter == null)
            throw new RuntimeException("defaultCostCenter is not found");
    }

    /**
     * Gets the money.
     * 
     * @param node the node
     * @return the money
     */
    private Money getMoney(Node node) {
        Double value = XMLUtils.getNodeAttributeDouble(node, "value", 0d);
        Money money = new Money(
                OperationRuleXmlParser.getCurrency(budget, node), value);
        return money;
    }

    /**
     * Load accounts.
     */
    private void loadAccounts() {
        AccountDomain ad = budget.getAccountDomain();
        Node node = XMLUtils.fundNodeByPath(XMLUtils.getDocumentRoot(doc),
                "/budget/Accounts");
        List<Node> childs = XMLUtils.getChildNodes(node);
        for (Node n : childs) {
            if (XMLUtils.isNodeNameEquals(n, "Account")) {
                Account a = ad.add(XMLUtils.getNodeId(n), getCostCenter(n),
                        getMoney(n));
                if (defaultAccount == null && isDefaultNode(n))
                    defaultAccount = a;
            }
        }

        if (defaultAccount == null)
            throw new RuntimeException("defaultAccount is not found");
    }

    /**
     * Load expenses.
     */
    private void loadExpenses() {
        budget.getOperationRuleDomain().expenses();
        List<Node> childs = XMLUtils.getChildNodes(XMLUtils
                .getDocumentRoot(doc));
        for (Node node : childs) {
            if (XMLUtils.isNodeNameEquals(node, EXPENSES)) {
                loadOperationCategory(createCategory(null, node), node,
                        EXPENSES);
            }
        }

    }

    /**
     * Load trasfers.
     */
    private void loadTrasfers() {
        budget.getOperationRuleDomain().trasfers();
        List<Node> childs = XMLUtils.getChildNodes(XMLUtils
                .getDocumentRoot(doc));
        for (Node node : childs) {
            if (XMLUtils.isNodeNameEquals(node, TRANSFERS)) {
                loadOperationCategory(createCategory(null, node), node,
                        TRANSFERS);
            }
        }

    }

    /**
     * Checks if is leaf node.
     * 
     * @param node the node
     * @return true, if is leaf node
     */
    private static boolean isLeafNode(Node node) {
        return null != XMLUtils.findFirstChildNode(node, RULE);
    }

    /**
     * Creates the category.
     * 
     * @param parent the parent
     * @param node the node
     * @return the operation category
     */
    private OperationCategory createCategory(OperationCategory parent, Node node) {
        OperationCategory result = null;
        String name = XMLUtils.getNodeId(node);
        if (isLeafNode(node))
            result = new OperationCategoryLeaf(name, budget);
        else
            result = new OperationCategory(name, budget);

        if (parent != null)
            parent.add(result);

        return result;
    }

    /**
     * Load operation category.
     * 
     * @param parentCategory the parent category
     * @param parentNode the parent node
     * @param categoryNodeName the category node name
     */
    private void loadOperationCategory(OperationCategory parentCategory,
            Node parentNode, String categoryNodeName) {
        List<Node> childNodes = XMLUtils.getChildNodes(parentNode);

        for (Node childNode : childNodes) {

            if (XMLUtils.isNodeNameEquals(childNode, categoryNodeName)) {
                OperationCategory category = createCategory(parentCategory,
                        childNode);
                loadOperationCategory(category, childNode, categoryNodeName);
            } else if (XMLUtils.isNodeNameEquals(childNode, RULE)) {
                addOperationRule((OperationCategoryLeaf) parentCategory,
                        childNode);
            }

        }
    }

    /**
     * Gets the account.
     * 
     * @param node the node
     * @return the account
     */
    private Account getAccount(Node node) {
        if (node == null)
            throw new IllegalArgumentException("node is null");
        if (defaultAccount == null)
            throw new IllegalStateException("defaultAccount is null");
        String accId = getNodeAttribute(node, "Account", defaultAccount.getId());
        Account a = budget.getAccountDomain().findById(accId);
        if (a == null)
            throw new RuntimeException("Can not found Account");
        return a;
    }

    /**
     * Gets the dest account.
     * 
     * @param node the node
     * @return the dest account
     */
    private Account getDestAccount(Node node) {
        String accId = getNodeAttribute(node, "ToAccount",
                defaultAccount.getId());
        Account a = budget.getAccountDomain().findById(accId);
        if (a == null)
            throw new RuntimeException("Can not found ToAccount");
        return a;
    }

    /**
     * Adds the operation rule.
     * 
     * @param categoryLeaf the category leaf
     * @param node the node
     * @return the operation rule
     */
    private OperationRule addOperationRule(OperationCategoryLeaf categoryLeaf,
            Node node) {
        OperationRuleXmlParser v = new OperationRuleXmlParser(node, budget);
        OperationRuleDomain ord = budget.getOperationRuleDomain();
        if (ord.getOperationTyp() == OperationRuleDomain.TRANSFER)
            return ord.add(v.getRuleId(), getAccount(node),
                    getDestAccount(node), categoryLeaf, v.getRuleActions());
        return ord.add(v.getRuleId(), getAccount(node), categoryLeaf,
                v.getRuleActions());

    }

    /**
     * Load incomes.
     */
    private void loadIncomes() {
        budget.getOperationRuleDomain().incomes();
        List<Node> childs = XMLUtils.getChildNodes(XMLUtils
                .getDocumentRoot(doc));
        for (Node node : childs) {
            if (XMLUtils.isNodeNameEquals(node, INCOMES)) {
                loadOperationCategory(createCategory(null, node), node, INCOMES);
            }
        }
    }
}
